package com.pine.app.module.security.oauth.provider.sso;

import com.pine.app.module.security.core.Authentication;
import com.pine.app.module.security.core.common.enums.ErrorType;
import com.pine.app.module.security.oauth.provider.sso.token.OAuth2SsoTokenHandler;
import com.pine.app.module.security.oauth.provider.token.OAuth2AccessToken;
import com.pine.app.module.security.oauth.provider.token.converter.AccessTokenConverter;
import com.pine.app.module.security.oauth.provider.AuthenticationManager;
import com.pine.app.module.security.oauth.provider.token.converter.DefaultAccessTokenConverter;
import com.pine.app.module.security.oauth.exception.AuthenticationException;
import com.pine.app.module.security.oauth.provider.OAuth2Authentication;
import com.pine.app.module.security.oauth.user.service.UserDetailService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
import java.util.Map;

/**
 * @author xiaoyuan
 * @create 2020/3/9 14:24
 **/

@Slf4j
@Data
public class RouteOauthAuthenticationManager implements AuthenticationManager {

    private RestOperations restTemplate = new RestTemplate();
    private String checkTokenEndpointUrl;
    private String clientId;
    private String clientSecret;
    private String tokenName = "token";

    private UserDetailService userDetailService;

    private AccessTokenConverter tokenConverter;

    private OAuth2SsoTokenHandler oAuth2SsoTokenHandler;


    public RouteOauthAuthenticationManager() {
        tokenConverter = new DefaultAccessTokenConverter(userDetailService);
        ((RestTemplate)this.restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
            public void handleError(ClientHttpResponse response) throws IOException {
                if(response.getRawStatusCode() != 403) {
                    super.handleError(response);
                }

            }
        });
    }

    public RouteOauthAuthenticationManager(AccessTokenConverter tokenConverter) {
        this.tokenConverter = tokenConverter;
    }

    public RouteOauthAuthenticationManager(UserDetailService userDetailService) {
        this(userDetailService, new DefaultAccessTokenConverter(userDetailService));
    }

    public RouteOauthAuthenticationManager(UserDetailService userDetailService, AccessTokenConverter tokenConverter) {
        this.tokenConverter = tokenConverter;
        this.userDetailService = userDetailService;
        ((RestTemplate)this.restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
            public void handleError(ClientHttpResponse response) throws IOException {
                if(response.getRawStatusCode() != 403) {
                    super.handleError(response);
                }

            }
        });
    }

    @Override
    public OAuth2AccessToken accessToken(Authentication authentication) throws IOException {
        if (authentication == null) {
            ErrorType.ACCESS_DENIED.throwThis(AuthenticationException::new);
        }
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        HttpServletResponse response = attributes.getResponse();
        return oAuth2SsoTokenHandler.getAccessToken(request,response);
    }

    @Override
    public OAuth2Authentication authentication(Authentication authentication) {
        if (authentication == null) {
            ErrorType.ACCESS_DENIED.throwThis(AuthenticationException::new);
        }
        return authentication((String) authentication.getPrincipal());
    }

    @Override
    public OAuth2Authentication authentication(String access_token) {
        if(StringUtils.isBlank(access_token)){
            ErrorType.ACCESS_DENIED.throwThis(AuthenticationException::new);
        }
        MultiValueMap<String, String> formData = new LinkedMultiValueMap();
        formData.add(this.tokenName, access_token);
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", this.getAuthorizationHeader(this.clientId, this.clientSecret));
        Map<String, Object> map = null;
        try{
            map = this.postForMap(this.checkTokenEndpointUrl, formData, headers);
        }catch (HttpClientErrorException e){
            log.error("校验token接口("+this.checkTokenEndpointUrl+")错误或者无法访问",e);
            throw new AuthenticationException(ErrorType.SERVICE_ERROR,"校验token接口("+this.checkTokenEndpointUrl+")错误或者无法访问");
        }
        if (map.containsKey("error") || Boolean.FALSE.equals(map.get("success"))) {
            if (this.log.isDebugEnabled()) {
                this.log.debug("check_token returned error: ");
            }
            throw new AuthenticationException(ErrorType.SERVICE_ERROR,"校验token接口出现错误");
        } else if (!Boolean.TRUE.equals(map.get("active"))) {
            this.log.debug("check_token returned active attribute: " + map.get("active"));
            throw new AuthenticationException(ErrorType.SERVICE_ERROR,access_token);
        } else {
            return this.tokenConverter.extractAuthentication(map);
        }
    }

    private String getAuthorizationHeader(String clientId, String clientSecret) {
        if (clientId == null || clientSecret == null) {
            this.log.warn("Null Client ID or Client Secret detected. Endpoint that requires authentication will reject request with 401 error.");
        }
        String creds = String.format("%s:%s", new Object[]{clientId, clientSecret});

        try {
            return "Basic " + new String(Base64.getEncoder().encode(creds.getBytes("UTF-8")));
        } catch (UnsupportedEncodingException var5) {
            throw new IllegalStateException("Could not convert String");
        }
    }


    private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
        if (headers.getContentType() == null) {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        }
        Map map = this.restTemplate.exchange(path, HttpMethod.POST, new HttpEntity(formData, headers), Map.class, new Object[0]).getBody();
        return map;
    }
}
